Add Finnish (fi) translations - #142
Conversation
Adds a complete Finnish locale following the structure of the existing
non-English locales (da/de/nl/no), with exact key parity against them:
- app.yml: 514 keys, reusing the YAML anchors so invoice translations
merge into the payment, delivery and order documents
- countries.yml: 249 country names
- currencies.yml: 166 currency names
- units.yml: 57 symbol-less units in partitive plural, so they read
correctly after a quantity ("20 tuntia", "5 kappaletta")
Also registers the locale so it actually ships and is covered:
- locales.go: add fi to the //go:embed directive, without which the
files are silently excluded from the binary
- units_test.go: add "fi" to both locale lists so the unit coverage
tests apply to it
Like the other non-English locales apart from es, the regime-specific
titles (regimes.*) are left untranslated and fall back to English.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
rilla
left a comment
There was a problem hiding this comment.
Nice one — the wiring is right and the Finnish reads like Finnish. I flattened all four files (anchors and merge keys resolved) and diffed key sets: exact 986-key match with da/nl/no, anchors on the same lines, so it really is a faithful mirror. Also rendered all 81 examples with ?locale=fi and grepped for missing-key markers — zero. Plurals are fine too: every locale gets the same one/other rule, which is what Finnish wants, and both forms are present.
Left inline suggestions for a few word-level things. Three I'd actually take (rate, 6pack, taxable_event), the rest are yours to judge.
Two notes that don't fit on a line:
The units are partitive singular, not partitive plural. "20 tuntia" is singular — the plural would be "tunteja". Every form in the file is correct, it's just the label in the description. Worth fixing so the next person translating a Finno-Ugric locale doesn't copy the wrong rule.
Key parity is this PR's main risk and there's nothing guarding it. The units test has real teeth — loading unmerged so the English fallback can't mask a gap is exactly right — but the other ~900 keys in app/countries/currencies are only covered by hand. The pattern is already there: same locale list, l.Has(...) over a key set derived from en (plus a sibling for currencies, which en doesn't have). Would make the next locale much cheaper. Related: the locale list now lives in three places (embed directive plus two test slices), so a locale that's embedded but missing from the slices is silently untested — deriving it from the embedded FS would close that.
I'd agree with skipping LocaleForExample, though worth noting the prefix there is a country code and FI happens to equal fi, so it's a one-line case "fi" whenever a Finnish example lands.
rilla
left a comment
There was a problem hiding this comment.
LGTM! Left some comments with nits (by Claude, of course, I don't speak Finnish!)
Word-level fixes from review: - rate: "taksa" -> "taksaa", the only nominative in an otherwise partitive file - one: "yksi" -> "kpl", since a nominative after a numeral is wrong in Finnish and "kpl" is what a Finnish invoice prints - 6pack: "kuuden pakkausta" -> "kuusipakkausta", the established word - demijohn: "lasipulloa" -> "koripulloa", which is the specific term - taxable_event: "verollinen" -> "verotettava", matching the Finnish text of the VAT directive for the chargeable event - cheque: "Šekki" -> "Sekki", modern orthography - CNY "yuan" -> "juan", GBP "Englannin" -> "Britannian" to match the country name, TWD word order Also translates five keys the older locales leave to the English fallback but which the renderer uses for every document: the party person and website labels (a Finnish order was rendering "Web:") and the adjustment invoice title. Adds TestLocaleKeyCoverage, which checks every locale against the English key set plus the union of currency codes, with regime and proper-noun keys exempted and pre-existing gaps listed in knownGaps. New locales must ship with no entry there. TestLocaleFilesUseOwnCode catches a file copied from another locale with its code left behind. The locale list now comes from locales.Codes(), derived from the embed directive, so an embedded locale can no longer be silently untested. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Thanks — pushed 645bc9e. All nine inline threads are answered and resolved; eight taken as suggested, Partitive singular — you're right, and the label was wrong in two places. "20 tuntia" is singular; "tunteja" would be the plural. Every form in the file was already the right one, but I'd described the rule wrong in both the PR body and the first commit message. PR body is corrected; the commit message I've left alone rather than rewriting pushed history. Key parity — added a guard.
I verified it has teeth rather than trusting it: deleting a key from And the locale list now comes from Writing that test surfaced real gaps in other locales. They're in
One deviation from the exact 🤖 Addressed by Claude Code |
The org.party.labels.* keys added in #143 are country tax ID abbreviations — CUIT, NIF, RFC, P.IVA — which are used as they are in every language, so no locale translates them. Only the generic labels.default is translated. Exempting them by rule keeps the knownGaps table for genuinely missing translations. Also composes knownGaps from named groups so each key literal appears once, and names the locale file constants, both of which goconst was flagging. Verified with golangci-lint 2.13.1 against the repo config: 0 issues. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Merged The new goconst. Worth flagging the maintenance shape this implies, since it's a cost the test imposes on everyone rather than just on new locales: adding an English string that locales don't yet translate will now fail CI until it's either translated, exempted, or recorded in 🤖 Addressed by Claude Code |
Adds Finnish as a supported language.
What changed
New locale in
locales/fi/, mirroring the structure of the existing non-English locales (da/de/nl/no):app.ymlcountries.ymlcurrencies.ymlunits.ymlPlus the wiring needed for it to actually ship and be covered:
locales/locales.go— addedfito the//go:embeddirective. Without this the files are silently excluded from the binary. Also addsCodes(), which derives the supported-language list from the embedded content so it only has to be maintained in one place.components/t/units_test.go— the two hardcoded locale slices now come fromlocales.Codes(), so an embedded locale can't be silently untested.Key parity guard
locales/locales_test.gois new, prompted by review:TestLocaleKeyCoveragechecks every locale against the English key set plus the union of currency codes (English deliberately has nocurrencies.yml).regimes.*and foreign proper nouns likeCodice fiscaleare exempt by rule; everything else that falls back to English today is listed per-locale inknownGaps. A new locale must ship with no entry there, and a stale entry also fails, so the list can only shrink.TestLocaleFilesUseOwnCodecatches a file copied from another locale with the original root key left behind — a mistake that would otherwise load silently under the wrong language.Writing the test surfaced pre-existing gaps in other locales. They're recorded in
knownGapsand left alone as out of scope, but three are bugs rather than missing translations:frspells three payment-term keys with hyphens so they never match,plhasprices_include_taxwhere the template asks forprices_include, andde/it/plare each missing a country code.Notes for reviewers
fitranslates five keys theda/nl/nofamily leaves to the English fallback:org.party.person,person_label,website,website_labelandbilling.invoice.title.adjustment. These aren't regime-specific — the renderer reaches for them in any locale, and a Finnish purchase order was printing "Web: https://…" in English.regimes.*keys (PT document codes, GR/CO/IT/SG titles, PL KSeF) are not translated, so a PT delivery note renders "Delivery note". This matches every non-English locale exceptes.org.party.labels.defaultis "ALV-tunnus" (VAT number) rather than a literal rendering of English "TIN", followingda/de/nl.LocaleForExamplecase was added ininternal/gallery/gallery.go— there's nofi-prefixed example, so it would be dead code. It's a one-linecase "fi"whenever a Finnish example lands, since the prefix is a country code and FI happens to equal fi.Testing
go vet ./...and the full suite pass. I rendered invoice, credit note, purchase order, delivery note, adjustment credit note and multi-method payment receipt examples withWithLocale("fi")and read the output text. The new parity test was checked against deliberate breakage rather than assumed correct: removing a key fromfi/app.ymlfails it, re-keyingfi/app.ymlasda:fails it, and a staleknownGapsentry fails it.🤖 Generated with Claude Code